package com.tools.cleanmaster.utils;

import java.io.BufferedInputStream;
import java.io.BufferedWriter;
import java.io.Closeable;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.reflect.Array;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.Callable;

/**
 * desc:
 * date: 2018/6/12
 * author: ancun
 */

public class DiskLruCache {
    static final String JOURNAL_FILE = "journal";
    static final String JOURNAL_FILE_TMP = "journal.tmp";
    static final String MAGIC = "libcore.io.DiskLruCache";
    static final String VERSION_1 = "1";
    static final long ANY_SEQUENCE_NUMBER = -1L;
    private static final String CLEAN = "CLEAN";
    private static final String DIRTY = "DIRTY";
    private static final String REMOVE = "REMOVE";
    private static final String READ = "READ";
    private static final Charset UTF_8 = Charset.forName("UTF-8");
    private static final int IO_BUFFER_SIZE = 8192;
    private final File directory;
    private final File journalFile;
    private final File journalFileTmp;
    private final int appVersion;
    private final long maxSize;
    private final int valueCount;
    private long size = 0L;
    private Writer journalWriter;
    private final LinkedHashMap<String, Entry> lruEntries = new LinkedHashMap(0, 0.75F, true);
    private int redundantOpCount;
    private boolean debug = false;

    private long nextSequenceNumber = 0L;

    private final Callable<Void> cleanupCallable = new Callable() {
        public Void call() throws Exception {
            synchronized (DiskLruCache.this) {
                if (DiskLruCache.this.journalWriter == null) {
                    return null;
                }
                DiskLruCache.this.trimToSize();
                if (DiskLruCache.this.journalRebuildRequired()) {
                    DiskLruCache.this.rebuildJournal();
                    DiskLruCache.this.redundantOpCount = 0;
                }
            }
            return null;
        }
    };

    private final Runnable forceFlushRunnable = new Runnable() {
        public void run() {
            try {
                synchronized (DiskLruCache.this) {
                    if (DiskLruCache.this.journalWriter == null) {
                        return;
                    }

                    if (DiskLruCache.this.journalRebuildRequired()) {
                      //  if (CommonConstants.IS_DEBUG) {
                            //LOG.logI("force flush journal " + DiskLruCache.this.directory + ", redundantOpCount=" + DiskLruCache.this.redundantOpCount + ", lruSize=" + DiskLruCache.this.lruEntries.size());
                      //  }
                        DiskLruCache.this.rebuildJournal();
                        DiskLruCache.this.redundantOpCount = 0;
                    } else {
                        DiskLruCache.this.journalWriter.flush();
                    }
                }
            } catch (IOException e) {
            }
        }
    };

    private static <T> T[] copyOfRange(T[] original, int start, int end) {
        int originalLength = original.length;
        if (start > end) {
            throw new IllegalArgumentException();
        }
        if ((start < 0) || (start > originalLength)) {
            throw new ArrayIndexOutOfBoundsException();
        }
        int resultLength = end - start;
        int copyLength = Math.min(resultLength, originalLength - start);
        Object[] result = (Object[]) Array.newInstance(original.getClass().getComponentType(), resultLength);

        System.arraycopy(original, start, result, 0, copyLength);
        return (T[]) result;
    }

    public static String readFully(Reader reader)
            throws IOException {
        try {
            StringWriter writer = new StringWriter();
            char[] buffer = new char[1024];
            int count;
            while ((count = reader.read(buffer)) != -1) {
                writer.write(buffer, 0, count);
            }
            return writer.toString();
        } finally {
            reader.close();
        }
    }

    public static String readAsciiLine(InputStream in)
            throws IOException {
        StringBuilder result = new StringBuilder(80);
        while (true) {
            int c = in.read();
            if (c == -1) {
                throw new EOFException();
            }
            if (c == 10) {
                break;
            }
            result.append((char) c);
        }
        int length = result.length();
        if ((length > 0) && (result.charAt(length - 1) == '\r')) {
            result.setLength(length - 1);
        }
        return result.toString();
    }

    public static void closeQuietly(Closeable closeable) {
        if (closeable != null) {
            try {
                closeable.close();
            } catch (RuntimeException rethrown) {
                throw rethrown;
            } catch (Exception ignored) {
            }
        }
    }

    public static void deleteContents(File dir)
            throws IOException {
        File[] files = dir.listFiles();
        if (files == null) {
            throw new IllegalArgumentException("not a directory: " + dir);
        }
        for (File file : files) {
            if (file.isDirectory()) {
                deleteContents(file);
            }
            if (!file.delete()) {
                throw new IOException("failed to delete file: " + file);
            }
        }
    }

    private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) {
        this.directory = directory;
        this.appVersion = appVersion;
        this.journalFile = new File(directory, "journal");
        this.journalFileTmp = new File(directory, "journal.tmp");
        this.valueCount = valueCount;
        this.maxSize = maxSize;
    }

    private void startService() {
        //ThreadPool.schedule(this.forceFlushRunnable, 60L, 180L, TimeUnit.SECONDS);
    }

    public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize) throws IOException {
        return open(directory, appVersion, valueCount, maxSize, false);
    }

    public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize, boolean debug)
            throws IOException {
        if (maxSize <= 0L) {
            throw new IllegalArgumentException("maxSize <= 0");
        }
        if (valueCount <= 0) {
            throw new IllegalArgumentException("valueCount <= 0");
        }

        DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
        cache.debug = debug;
        if (cache.journalFile.exists()) {
            try {
                cache.readJournal();
                cache.processJournal();
                cache.journalWriter = new BufferedWriter(new FileWriter(cache.journalFile, true), 8192);

                return cache;
            } catch (IOException journalIsCorrupt) {
                cache.delete();
            }

        }

        directory.mkdirs();
        cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
        cache.rebuildJournal();
        cache.startService();
        return cache;
    }

    private void readJournal() throws IOException {
        InputStream in = new BufferedInputStream(new FileInputStream(this.journalFile), 8192);
        try {
            String magic = readAsciiLine(in);
            String version = readAsciiLine(in);
            String appVersionString = readAsciiLine(in);
            String valueCountString = readAsciiLine(in);
            String blank = readAsciiLine(in);
            if ((!"libcore.io.DiskLruCache".equals(magic)) || (!"1".equals(version)) || (!Integer.toString(this.appVersion).equals(appVersionString)) || (!Integer.toString(this.valueCount).equals(valueCountString)) || (!"".equals(blank))) {
                throw new IOException("unexpected journal header: [" + magic + ", " + version + ", " + valueCountString + ", " + blank + "]");
            }
            try {
                while (true) {
                    readJournalLine(readAsciiLine(in));
                }
            } catch (EOFException endOfJournal) {
            }
        } catch (Throwable tr) {
            //LOG.logE("error loading existing journal: " + this.directory + ", skip remaining.");
        } finally {
            closeQuietly(in);
        }

        if (this.debug) {
            //LOG.logI("read: " + this.lruEntries.size() + " entries from journal");
        }
    }

    private void readJournalLine(String line) throws IOException {
        String[] parts = line.split(" ");
        if (parts.length < 2) {
            throw new IOException("unexpected journal line: " + line);
        }

        if (this.debug) {
            //LOG.logI(line);
        }

        String key = parts[1];
        if ((parts[0].equals("REMOVE")) && (parts.length == 2)) {
            this.lruEntries.remove(key);
            return;
        }

        Entry entry = this.lruEntries.get(key);
        if (entry == null) {
            entry = new Entry(key);
            this.lruEntries.put(key, entry);
        }

        if (parts[0].equals("CLEAN")) {
            if ((parts.length == 3 + this.valueCount) || (parts.length == 4 + this.valueCount)) {
                boolean hasTag = parts.length == 4 + this.valueCount;
                int length = hasTag ? parts.length : parts.length + 1;

                if (this.debug) {
                    //LOG.logI("LOAD: " + line + " - hasTag=" + hasTag);
                }

                entry.readable = true;
                entry.currentEditor = null;
                entry.setLengths(copyOfRange(parts, 2, length - 2));
                entry.lastModify = Long.parseLong(parts[(length - 2)]);

                entry.tag = (hasTag ? URLDecoder.decode(parts[(length - 1)]) : null);
                entry.tag = (entry.tag == null ? "" : entry.tag);
            } else {
                //LOG.logE("bad entry: length=" + parts.length + ", value=" + this.valueCount + " line=" + line);
            }
        } else if ((parts[0].equals("DIRTY")) && (parts.length == 2)) {
            entry.currentEditor = new Editor(entry);
        } else if ((!parts[0].equals("READ")) || (parts.length != 2)) {
            throw new IOException("unexpected journal line: " + line);
        }
    }

    private void processJournal()
            throws IOException {
        deleteIfExists(this.journalFileTmp);
        for (Iterator i = this.lruEntries.values().iterator(); i.hasNext(); ) {
            Entry entry = (Entry) i.next();
            if (entry.currentEditor == null) {
                for (int t = 0; t < this.valueCount; t++)
                    this.size += entry.lengths[t];
            } else {
                entry.currentEditor = null;
                for (int t = 0; t < this.valueCount; t++) {
                    deleteIfExists(entry.getCleanFile(t));
                    deleteIfExists(entry.getDirtyFile(t));
                }
                i.remove();
            }
        }
    }

    private synchronized void rebuildJournal()
            throws IOException {
        if (this.journalWriter != null) {
            this.journalWriter.close();
        }

       // if (CommonConstants.IS_DEBUG) {
            //LOG.logI("now rebuild journal: " + this.directory.getCanonicalPath());
        //}

        Writer writer = new BufferedWriter(new FileWriter(this.journalFileTmp), 8192);
        writer.write("libcore.io.DiskLruCache");
        writer.write("\n");
        writer.write("1");
        writer.write("\n");
        writer.write(Integer.toString(this.appVersion));
        writer.write("\n");
        writer.write(Integer.toString(this.valueCount));
        writer.write("\n");
        writer.write("\n");

        StringBuilder sb = new StringBuilder();

        for (Entry entry : this.lruEntries.values()) {
            try {
                if (entry.currentEditor != null) {
                    writer.write("DIRTY " + entry.key + '\n');
                } else {
                    sb.setLength(0);
                    sb.append("CLEAN").append(' ').append(entry.key).append(entry.getLengths()).append(' ').append(entry.lastModify).append(' ').append(URLEncoder.encode(entry.tag)).append('\n');

                    writer.write(sb.toString());
                }
            } catch (Exception e) {
               // if (CommonConstants.IS_DEBUG) {
                    //LOG.logE("", e);
               // }
            }
        }
        writer.close();
        this.journalFileTmp.renameTo(this.journalFile);
        this.journalWriter = new BufferedWriter(new FileWriter(this.journalFile, true), 8192);
    }

    private static void deleteIfExists(File file)
            throws IOException {
        if ((file.exists()) && (!file.delete())) {
            throw new IOException();
        }
    }

    public synchronized Snapshot get(String key)
            throws IOException {
        checkNotClosed();
        validateKey(key);
        Entry entry = this.lruEntries.get(key);
        if (entry == null) {
            return null;
        }

        if (!entry.readable) {
            return null;
        }

        File[] files = new File[this.valueCount];
        InputStream[] ins = new InputStream[this.valueCount];
        try {
            for (int i = 0; i < this.valueCount; i++) {
                files[i] = entry.getCleanFile(i);
                ins[i] = new FileInputStream(files[i]);
            }
        } catch (FileNotFoundException e) {
            return null;
        }

        this.redundantOpCount += 1;
        this.journalWriter.append("READ " + key + '\n');

        if (journalRebuildRequired()) {
            /*ThreadPool.runOnWorker(new Runnable() {
                public void run() {
                    try {
                        DiskLruCache.this.cleanupCallable.call();
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    }
                }
            });*/
        }

        return new Snapshot(key, entry.sequenceNumber, ins, files, entry.lastModify, entry.tag);
    }

    public Editor edit(String key)
            throws IOException {
        return edit(key, -1L);
    }

    private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
        checkNotClosed();
        validateKey(key);
        Entry entry = this.lruEntries.get(key);
        if ((expectedSequenceNumber != -1L) && ((entry == null) || (entry.sequenceNumber != expectedSequenceNumber))) {
            return null;
        }
        if (entry == null) {
            entry = new Entry(key);
            this.lruEntries.put(key, entry);
        } else if (entry.currentEditor != null) {
            return null;
        }

        Editor editor = new Editor(entry);
        entry.currentEditor = editor;

        this.journalWriter.write("DIRTY " + key + '\n');
        this.journalWriter.flush();
        return editor;
    }

    public File getDirectory() {
        return this.directory;
    }

    public long maxSize() {
        return this.maxSize;
    }

    public synchronized long size() {
        return this.size;
    }

    private synchronized void completeEdit(Editor editor, boolean success, long lastModify, String tag) throws IOException {
        Entry entry = editor.entry;

        entry.lastModify = lastModify;
        entry.tag = (tag == null ? "" : tag);

        if (entry.currentEditor != editor) {
            throw new IllegalStateException();
        }

        if ((success) && (!entry.readable)) {
            for (int i = 0; i < this.valueCount; i++) {
                if (!entry.getDirtyFile(i).exists()) {
                    editor.abort();
                    throw new IllegalStateException("edit didn't create file " + i);
                }
            }
        }

        for (int i = 0; i < this.valueCount; i++) {
            File dirty = entry.getDirtyFile(i);
            if (success) {
                if (dirty.exists()) {
                    File clean = entry.getCleanFile(i);
                    dirty.renameTo(clean);
                    long oldLength = entry.lengths[i];
                    long newLength = clean.length();
                    entry.lengths[i] = newLength;
                    this.size = (this.size - oldLength + newLength);
                }
            } else {
                deleteIfExists(dirty);
            }

        }

        this.redundantOpCount += 1;
        entry.currentEditor = null;
        if ((entry.readable | success)) {
            entry.readable = true;

            StringBuilder sb = new StringBuilder();
            sb.append("CLEAN").append(' ').append(entry.key).append(entry.getLengths()).append(' ').append(entry.lastModify).append(' ').append(URLEncoder.encode(entry.tag)).append('\n');

            this.journalWriter.write(sb.toString());

            if (success) {
                entry.sequenceNumber = (this.nextSequenceNumber++);
            }
        } else {
            this.lruEntries.remove(entry.key);
            this.journalWriter.write("REMOVE " + entry.key + '\n');
        }

        this.journalWriter.flush();

        if ((this.size > this.maxSize) || (journalRebuildRequired())) {
            /*ThreadPool.runOnWorker(new Runnable() {
                public void run() {
                    try {
                        DiskLruCache.this.cleanupCallable.call();
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    }
                }
            });*/
        }
    }

    private boolean journalRebuildRequired() {
        int REDUNDANT_OP_COMPACT_THRESHOLD = 500;
        return (this.redundantOpCount >= 500) && (this.redundantOpCount >= this.lruEntries.size());
    }

    public synchronized boolean remove(String key)
            throws IOException {
        checkNotClosed();
        validateKey(key);
        Entry entry = this.lruEntries.get(key);
        if ((entry == null) || (entry.currentEditor != null)) {
            return false;
        }

        for (int i = 0; i < this.valueCount; i++) {
            File file = entry.getCleanFile(i);
            if (!file.delete()) {
                throw new IOException("failed to delete " + file);
            }
            this.size -= entry.lengths[i];
            entry.lengths[i] = 0L;
        }

        this.redundantOpCount += 1;
        this.journalWriter.append("REMOVE " + key + '\n');
        this.lruEntries.remove(key);

        if (journalRebuildRequired()) {
            /*ThreadPool.runOnWorker(new Runnable() {
                public void run() {
                    try {
                        DiskLruCache.this.cleanupCallable.call();
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    }
                }
            });*/
        }

        return true;
    }

    public boolean isClosed() {
        return this.journalWriter == null;
    }

    private void checkNotClosed() {
        if (this.journalWriter == null) {
            throw new IllegalStateException("cache is closed");
        }
    }

    public synchronized void flush()
            throws IOException {
        checkNotClosed();
        trimToSize();
        this.journalWriter.flush();
    }

    public synchronized void close()
            throws IOException {
        if (this.journalWriter == null) {
            return;
        }
        for (Entry entry : new ArrayList<Entry>(this.lruEntries.values())) {
            if (entry.currentEditor != null) {
                entry.currentEditor.abort();
            }
        }
        trimToSize();
        this.journalWriter.close();
        this.journalWriter = null;
    }

    private void trimToSize() throws IOException {
        while (this.size > this.maxSize) {
            Map.Entry toEvict = this.lruEntries.entrySet().iterator().next();
            remove((String) toEvict.getKey());
        }
    }

    public void delete()
            throws IOException {
        close();
        deleteContents(this.directory);
    }

    private void validateKey(String key) {
        if ((key.contains(" ")) || (key.contains("\n")) || (key.contains("\r"))) {
            throw new IllegalArgumentException("keys must not contain spaces or newlines: \"" + key + "\"");
        }
    }

    private static String inputStreamToString(InputStream in)
            throws IOException {
        return readFully(new InputStreamReader(in, UTF_8));
    }

    private final class Entry {
        private final String key;
        private final long[] lengths;
        private boolean readable;
        private DiskLruCache.Editor currentEditor;
        private long sequenceNumber;
        private long lastModify = -1L;

        private String tag = "";

        private Entry(String key) {
            this.key = key;
            this.lengths = new long[DiskLruCache.this.valueCount];
        }

        public String getLengths() {
            StringBuilder result = new StringBuilder();
            for (long size : this.lengths) {
                result.append(' ').append(size);
            }
            return result.toString();
        }

        private void setLengths(String[] strings)
                throws IOException {
            if (strings.length != DiskLruCache.this.valueCount) {
                throw invalidLengths(strings);
            }
            try {
                for (int i = 0; i < strings.length; i++)
                    this.lengths[i] = Long.parseLong(strings[i]);
            } catch (NumberFormatException e) {
                throw invalidLengths(strings);
            }
        }

        private IOException invalidLengths(String[] strings) throws IOException {
            throw new IOException("unexpected journal line: " + Arrays.toString(strings));
        }

        public File getCleanFile(int i) {
            return new File(DiskLruCache.this.directory, this.key + "." + i);
        }

        public File getDirtyFile(int i) {
            return new File(DiskLruCache.this.directory, this.key + "." + i + ".tmp");
        }
    }

    public final class Editor {
        private final DiskLruCache.Entry entry;
        private boolean hasErrors;

        private Editor(DiskLruCache.Entry entry) {
            this.entry = entry;
        }

        public InputStream newInputStream(int index)
                throws IOException {
            synchronized (DiskLruCache.this) {
                if (this.entry.currentEditor != this) {
                    throw new IllegalStateException();
                }
                if (!this.entry.readable) {
                    return null;
                }
                return new FileInputStream(this.entry.getCleanFile(index));
            }
        }

        public String getString(int index)
                throws IOException {
            InputStream in = newInputStream(index);
            return in != null ? DiskLruCache.inputStreamToString(in) : null;
        }

        public OutputStream newOutputStream(int index)
                throws IOException {
            synchronized (DiskLruCache.this) {
                if (this.entry.currentEditor != this) {
                    throw new IllegalStateException();
                }
                return new FaultHidingOutputStream(new FileOutputStream(this.entry.getDirtyFile(index)));
            }
        }

        public void set(int index, String value)
                throws IOException {
            Writer writer = null;
            try {
                writer = new OutputStreamWriter(newOutputStream(index), DiskLruCache.UTF_8);
                writer.write(value);
            } finally {
                DiskLruCache.closeQuietly(writer);
            }
        }

        public void commit()
                throws IOException {
            if (this.hasErrors) {
                DiskLruCache.this.completeEdit(this, false, System.currentTimeMillis(), "");
                DiskLruCache.this.remove(this.entry.key);
            } else {
                DiskLruCache.this.completeEdit(this, true, System.currentTimeMillis(), "");
            }
        }

        public void commit(long lastModified, String tag) throws IOException {
            if (this.hasErrors) {
                DiskLruCache.this.completeEdit(this, false, lastModified, tag);
                DiskLruCache.this.remove(this.entry.key);
            } else {
                DiskLruCache.this.completeEdit(this, true, lastModified, tag);
            }
        }

        public boolean hasErrors() {
            return this.hasErrors;
        }

        public void abort()
                throws IOException {
            DiskLruCache.this.completeEdit(this, false, System.currentTimeMillis(), "");
        }

        public class FaultHidingOutputStream extends FilterOutputStream {
            private FaultHidingOutputStream(OutputStream out) {
                super(out);
            }

            public void write(int oneByte) {
                try {
                    this.out.write(oneByte);
                } catch (IOException e) {
                    DiskLruCache.Editor.this.hasErrors = true;
                }
            }

            public void write(byte[] buffer, int offset, int length) {
                try {
                    this.out.write(buffer, offset, length);
                } catch (IOException e) {
                    DiskLruCache.Editor.this.hasErrors = true;
                }
            }

            public void close() {
                try {
                    this.out.close();
                } catch (IOException e) {
                    DiskLruCache.Editor.this.hasErrors = true;
                }
            }

            public void flush() {
                try {
                    this.out.flush();
                } catch (IOException e) {
                    DiskLruCache.Editor.this.hasErrors = true;
                }
            }
        }
    }

    public final class Snapshot implements Closeable {
        private final String key;
        private final long sequenceNumber;
        private final InputStream[] ins;
        private final File[] files;
        private final long lastModify;
        private final String tag;

        private Snapshot(String key, long sequenceNumber, InputStream[] ins, File[] files, long lastModify, String tag) {
            this.key = key;
            this.sequenceNumber = sequenceNumber;
            this.ins = ins;
            this.files = files;
            this.lastModify = lastModify;
            this.tag = tag;
        }

        public DiskLruCache.Editor edit()
                throws IOException {
            return DiskLruCache.this.edit(this.key, this.sequenceNumber);
        }

        public InputStream getInputStream(int index) {
            return this.ins[index];
        }

        public File getFile(int index) {
            return this.files[index];
        }

        public String getString(int index)
                throws IOException {
            return DiskLruCache.inputStreamToString(getInputStream(index));
        }

        public void close() {
            for (InputStream in : this.ins)
                DiskLruCache.closeQuietly(in);
        }

        public long lastModified() {
            return this.lastModify;
        }

        public String getTag() {
            return this.tag;
        }
    }
}